package main

import (
	"sync"
)

// 演示资源竞争
/**
多个协程访问同一块内存，会出现资源竞争的情况
*/
var sum = 0

//var mutex sync.Mutex
var mutex sync.RWMutex

//func main() {
//	// 开启100个协程，每个协程让sum+1
//	for i := 0; i < 100; i++ {
//		go func() {
//			sum += 1
//		}()
//	}
//	// 防止提前退出
//	time.Sleep(time.Duration(2) * time.Second)
//	fmt.Println("和为：", sum) // 和会出现 99 和 100的情况
//	// 导致这种情况的核心原因是资源 sum 不是并发安全的，因为同时会有多个协程交叉执行 sum+=i，产生不可预料的结果。
//	// 可能当sum是1的时候，有多个协程同时执行了 sum+=1 ，得出的结果是sum = 2
//
//	// 使用 go build、go run、go test 这些 Go 语言工具链提供的命令时，添加 -race 标识可以帮你检查 Go 语言代码是否存在资源竞争。
//	// go run -race  01_sync.go
//}

//func main() {
//	for i := 0; i < 100; i++ {
//		go add(1)
//	}
//	// 防止提前退出
//	time.Sleep(time.Duration(2) * time.Second)
//	fmt.Println("和为：", sum) // 因为add的方法中加入了互斥锁，所以是线程安全的，每次只会有一个协程去执行此段代码，sum最后一定是100
//}

func add(i int) {
	/**
	小提示：以上被加锁保护的 sum+=i 代码片段又称为临界区。在同步的程序设计中，
	临界区段指的是一个访问共享资源的程序片段，而这些共享资源又有无法同时被多个协程访问的特性。
	当有协程进入临界区段时，其他协程必须等待，这样就保证了临界区的并发安全。
	*/
	mutex.Lock()
	sum += i
	mutex.Unlock()
	wg.Done()
}

/**
互斥锁的使用非常简单，它只有两个方法 Lock 和 Unlock，代表加锁和解锁。当一个协程获得 Mutex 锁后，其他协程只能等到 Mutex 锁释放后才能再次获得锁。
Mutex 的 Lock 和 Unlock 方法总是成对出现，而且要确保 Lock 获得锁后，一定执行 UnLock 释放锁，所以在函数或者方法中会采用 defer 语句释放锁，如下面的代码所示：
*/

func subtract(i int) {
	mutex.Lock()
	// 这样可以确保锁一定会被释放，不会被遗忘。
	defer mutex.Unlock()
	sum -= i
}

// sync.RWMutex
/**
在 sync.Mutex 小节中，我对共享资源 sum 的加法操作进行了加锁，这样可以保证在修改 sum 值的时候是并发安全的。如果读取操作也采用多个协程呢？如下面的代码所示：
*/
var wg sync.WaitGroup

//func main() {
//	wg.Add(110)
//
//	for i := 0; i < 100; i++ {
//		go add(1)
//	}
//	for i := 0; i < 10; i++ {
//		go fmt.Println("现在sum的值为：",readSum())
//	}
//	//time.Sleep(2 * time.Second)
//	// sync.WaitGroup
//	/**
//	在以上示例中，相信你注意到了这段 time.Sleep(2 * time.Second) 代码，这是为了防止主函数 main 返回使用，一旦 main 函数返回了，程序也就退出了。
//	因为我们不知道 100 个执行 add 的协程和 10 个执行 readSum 的协程什么时候完全执行完毕，所以设置了一个比较长的等待时间，也就是两秒。
//	一个函数或者方法的返回 (return) 也就意味着当前函数执行完毕。
//
//	所以存在一个问题，如果这 110 个协程在两秒内执行完毕，main 函数本该提前返回，但是偏偏要等两秒才能返回，会产生性能问题。
//	如果这 110 个协程执行的时间超过两秒，因为设置的等待时间只有两秒，程序就会提前返回，导致有协程没有执行完毕，产生不可预知的结果。
//	Go 语言为我们提供了更简洁的解决办法，它就是 sync.WaitGroup。
//	*/
//	wg.Wait()
//}
//增加了一个读取sum的函数，便于演示并发
//func readSum() int{
//	/**
//	这个示例开启了 10 个协程，它们同时读取 sum 的值。因为 readSum 函数并没有任何加锁控制，所以它不是并发安全的，
//	即一个 goroutine 正在执行 sum+=i 操作的时候，另一个 goroutine 可能正在执行 b:=sum 操作，这就会导致读取的 num 值是一个过期的值，结果不可预期。
//
//	如果读和写不是串行执行的，则可能会出现脏读和幻读
//	 */
//	b:=sum
//	return b
//}
//
//func readSum() int {
//	/**
//	因为 add 和 readSum 函数使用的是同一个 sync.Mutex，所以它们的操作是互斥的，
//	也就是一个 goroutine 进行修改操作 sum+=i 的时候，另一个 gouroutine 读取 sum 的操作 b:=sum 会等待，直到修改操作执行完毕。
//	 */
//	mutex.Lock()
//	defer mutex.Unlock()
//	b:=sum
//	return b
//}

/**
现在我们分析读写这个特殊场景，有以下几种情况：
写的时候不能同时读，因为这个时候读取的话可能读到脏数据（不正确的数据）；
读的时候不能同时写，因为也可能产生不可预料的结果；
读的时候可以同时读，因为数据不会改变，所以不管多少个 goroutine 读都是并发安全的。
所以就可以通过读写锁 sync.RWMutex 来优化这段代码，提升性能。现在我将以上示例改为读写锁，来实现我们想要的结果，如下所示：
*/

func readSum() int {
	/**
	对比互斥锁的示例，读写锁的改动有两处：
	把锁的声明换成读写锁 sync.RWMutex。
	把函数 readSum 读取数据的代码换成读锁，也就是 RLock 和 RUnlock。
	这样性能就会有很大的提升，因为多个 goroutine 可以同时读数据，不再相互等待。

	总结：原来读的时候也是互斥的，这就导致读的时候只能有一个goroutine在执行，换上RLock之后，可以支持同时读
	*/
	mutex.RLock()
	defer mutex.RUnlock()
	b := sum
	wg.Done()
	return b
}
